const mongoose = require('mongoose');
const FunctionRelation = require('./mongo/function-relation');
const Function = require('./mongo/function');

async function getRelationsByVersion(versionId) {
  const relations = await FunctionRelation.find({ versionId: mongoose.Types.ObjectId(versionId) })
    .lean();
  return relations;
}

async function createRelations(
  fromFuncId,
  fromFuncMangledId,
  fromFuncFileId,
  toFuncMangledId,
  toFuncName,
  toFuncPointer,
  versionId
) {
  versionId = mongoose.Types.ObjectId(versionId);
  fromFuncId = mongoose.Types.ObjectId(fromFuncId);
  fromFuncFileId = mongoose.Types.ObjectId(fromFuncFileId);
  if (toFuncMangledId) {
    let toFuncFileId = fromFuncFileId;
    let toFuncId = null;
    const funcs = await Function.find({ versionId, mangledId: toFuncMangledId }, ['_id', 'fileId']);
    funcs.forEach((func) => {
      if (!toFuncId) {
        toFuncId = func._id;
        toFuncFileId = func.fileId;
      }
      // 调用函数所在文件的匹配到的函数优先级最高
      if (fromFuncFileId.toString() === func.fileId.toString()) {
        toFuncId = func._id;
        toFuncFileId = func.fileId;
      }
    });
    await FunctionRelation.collection.insertMany([{
      versionId,
      fromFuncId,
      fromFuncMangledId,
      fromFuncFileId,
      toFuncId,
      toFuncName,
      toFuncMangledId,
      toFuncFileId,
      toFuncPointer,
    }]).catch(() => {});
  }
  await FunctionRelation.updateMany(
    {
      versionId,
      toFuncId: null,
      toFuncMangledId: fromFuncMangledId,
    },
    {
      $set: {
        toFuncId: fromFuncId,
        toFuncFileId: fromFuncFileId,
      },
    },
  );
}

async function removeRelations(fromFuncId, toFuncMangledId, versionId) {
  await FunctionRelation.deleteMany({
    versionId: mongoose.Types.ObjectId(versionId),
    fromFuncId: mongoose.Types.ObjectId(fromFuncId),
    toFuncMangledId,
  });
}

async function removeRelationsOfFunctions(functionIds) {
  await FunctionRelation.deleteMany({ fromFuncId: { $in: functionIds } });
  const funcs = await FunctionRelation.find({ toFuncId: { $in: functionIds } });
  await FunctionRelation.deleteMany({ _id: { $in: funcs.map((func) => func._id) } });
  if (funcs.length === 0) {
    return;
  }
  await Promise.resolve(funcs.map((func) => createRelations(
    func.fromFuncId,
    func.fromFuncMangledId,
    func.fromFuncFileId,
    func.toFuncMangledId,
    func.toFuncName,
    func.toFuncPointer,
    func.versionId
  )));
}

async function removeRelationsOfVersions(versionIds) {
  await FunctionRelation.deleteMany({ versionId: { $in: versionIds } });
}

async function updateManualStub(functionId, versionId, fromStubName, toStubName) {
  await FunctionRelation.updateMany(
    {
      versionId: mongoose.Types.ObjectId(versionId),
      fromFuncId: mongoose.Types.ObjectId(functionId),
      toFuncMangledId: fromStubName,
    },
    {
      $set: {
        toFuncMangledId: toStubName,
        toFuncName: toStubName,
      },
    }
  );
}

async function retrieveRelations(functionId, versionId, relations = null) {
  functionId = mongoose.Types.ObjectId(functionId);
  if (!relations) {
    relations = await getRelationsByVersion(versionId);
  }
  const relationsMap = new Map();
  relations.forEach((relation) => {
    if (!relationsMap.has(relation.fromFuncId.toString())) {
      relationsMap.set(relation.fromFuncId.toString(), []);
    }
    relationsMap.get(relation.fromFuncId.toString()).push(relation);
  });
  const childFuncMap = new Map();
  function recurseCheck(functionId) {
    if (!relationsMap.has(functionId)) {
      return;
    }
    relationsMap.get(functionId).forEach((child) => {
      if (childFuncMap.has(child._id.toString())) {
        return;
      }
      childFuncMap.set(child._id.toString(), child);
      if (!child.toFuncId) {
        return;
      }
      recurseCheck(child.toFuncId.toString());
    });
  }
  recurseCheck(functionId.toString());
  return [...childFuncMap.values()];
}

async function getParentFuncs(functionIds, versionId) {
  const relationsMap = new Map();
  const relations = await getRelationsByVersion(versionId);
  relations.forEach((relation) => {
    if (!relation.toFuncId) {
      return;
    }
    if (!relationsMap.has(relation.toFuncId.toString())) {
      relationsMap.set(relation.toFuncId.toString(), []);
    }
    relationsMap.get(relation.toFuncId.toString()).push(relation.fromFuncId.toString());
  });
  const functionIdSet = new Set();
  function recurseCheck(functionId) {
    if (!relationsMap.has(functionId)) {
      return;
    }
    relationsMap.get(functionId).forEach((fromFuncId) => {
      if (functionIdSet.has(fromFuncId)) {
        return;
      }
      functionIdSet.add(fromFuncId);
      recurseCheck(fromFuncId);
    });
  }
  functionIds.forEach((functionId) => {
    recurseCheck(functionId.toString());
  });
  return [...functionIdSet];
}

module.exports = {
  createRelations,
  getParentFuncs,
  getRelationsByVersion,
  removeRelations,
  removeRelationsOfVersions,
  removeRelationsOfFunctions,
  retrieveRelations,
  updateManualStub,
};
exports = Object.assign(exports, module.exports);
